package model;

import java.util.ArrayList;

/**
 * This class implements to check the formation of the ladder in each grid configuration.
 *
 */
public class LadderGame
{
    public final static int GRID_ROW_SIZE = 7;
    public final static int GRID_COLUMN_SIZE = 13;

    private ArrayList<ArrayList<GamePiece>> grid;

    private LadderGameListener listener;

    private Player blackPlayer;
    private Player whitePlayer;
    private Player currentPlayer;

    private boolean gameIsOver;

    public LadderGame(LadderGameListener listener)
    {
        this.grid = new ArrayList<ArrayList<GamePiece>>(GRID_ROW_SIZE);

        for (int i = 0; i < GRID_ROW_SIZE; i++)
        {
            int columnSize = 2 * (GRID_ROW_SIZE - i) - 1;
            this.grid.add(i, new ArrayList<GamePiece>(columnSize));

            for (int j = 0; j < columnSize; j++)
            {
                this.grid.get(i).add(j, GamePiece.NO_PIECE);
            }
        }

        this.setListener(listener);
    }

    /**
     * Resets all GamePiece on the grid to NO_PIECE.
     * 
     * @param blackPlayer
     * @param whitePlayer
     */
    public void NewGame(Player blackPlayer, Player whitePlayer)
    {
        this.blackPlayer = blackPlayer;
        this.whitePlayer = whitePlayer;
        this.currentPlayer = blackPlayer;

        this.gameIsOver = false;

        for (int i = 0; i < this.grid.size(); i++)
        {
            for (int j = 0; j < this.grid.get(i).size(); j++)
            {
                this.grid.get(i).set(j, GamePiece.NO_PIECE);
            }
        }

        this.listener.onNewGame();
    }

    /**
     * Adds a new Piece to the grid. If the move is not legal, the grid will not
     * change and listener#OnMoveIsIllegal will be called.
     * 
     * @param position
     *            slot position on which to put the player's piece (ex : "H1",
     *            "B2").
     */
    public void insertPiece(String position)
    {
        try
        {
            // Get Row and Column Index from position String.
            int[] results = LadderGameHelper.getIndexPositionFromString(position);
            int rowIndex = results[0];
            int columnIndex = results[1];

            if (this.moveIsLegal(rowIndex, columnIndex))
            {
                GamePiece playerPiece = this.getGamePieceForPlayer(currentPlayer);
                this.grid.get(rowIndex).set(columnIndex, playerPiece);

                if (this.playerWins(currentPlayer))
                {
                    this.gameIsOver = true;
                    this.listener.onPlayerWins(currentPlayer);
                }
                else if (this.gridIsFull())
                {
                    this.gameIsOver = true;
                    this.listener.onTie();
                }
                else
                {
                    Player finishedPlayer = this.switchTurn();
                    this.listener.onPlayerTurnFinished(finishedPlayer, position.toUpperCase());
                }
            }
            else
            {
                this.listener.onIllegalMove(position, currentPlayer);
            }
        }
        catch (IndexOutOfBoundsException e)
        {
            this.listener.onIllegalMove(position, currentPlayer);
        }
        catch (Exception e)
        {
            this.listener.onErrorOccured(e.getMessage());
        }
    }

    /**
     * Returns the Gamepiece Corresponding to the passed player
     * 
     * @param player
     */
    public GamePiece getGamePieceForPlayer(Player player) throws Exception
    {               
        if (player == this.blackPlayer)
        {
            return GamePiece.BLACK;
        }
        else if (player == this.whitePlayer)
        {
            return GamePiece.WHITE;
        }
        else
        {
            throw new Exception("This Player does not Exists");
        }
    }
    
    /**
     * Returns the current opponent of the passed player.
     * 
     * @param player
     * @return
     */
    public Player getOpponentPlayer(Player player) throws Exception
    {
        if (player == this.blackPlayer)
        {
            return this.whitePlayer;
        }
        else if (player == this.whitePlayer)
        {
            return this.blackPlayer;
        }
        else
        {
            throw new Exception("This Player does not Exists");
        }        
    }

    /**
     * Returns true if adding a piece at the specified position would be a legal
     * move.
     * 
     * @param rowIndex
     * @param columnIndex
     * @return
     * @throws Exception
     */
    private boolean moveIsLegal(int rowIndex, int columnIndex) throws Exception
    {
        if (this.gameIsOver)
        {
            throw new Exception("The game is already over");
        }
        return this.grid.get(rowIndex).get(columnIndex) == GamePiece.NO_PIECE;
    }

    /**
     * Returns true if there is no more NO_PIECE slots on the grid, false
     * otherwise.
     * 
     * @return
     */
    private boolean gridIsFull()
    {
        for (int i = 0; i < this.grid.size(); i++)
        {
            for (int j = 0; j < this.grid.get(i).size(); j++)
            {
                if (this.grid.get(i).get(j) == GamePiece.NO_PIECE)
                {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * returns true if the passed player wins the game, false otherwise
     * 
     * @param player
     * @return
     * @throws Exception
     *             if the passed player is not the white or black player.
     */
    private boolean playerWins(Player player) throws Exception
    {
        GamePiece playerPiece = this.getGamePieceForPlayer(player);

        // Try to find a ladder pattern on the grid
        for (int i = 0; i < this.grid.size(); i++)
        {
            for (int j = 0; j < this.grid.get(i).size(); j++)
            {
                Slot[][] patterns = this.findLadderPatternsAtPosition(i, j, playerPiece);

                if (patterns[0] != null && !this.ladderIsNeutralized(patterns[0], playerPiece, true))
                {
                    return true;
                }

                if (patterns[1] != null && !this.ladderIsNeutralized(patterns[1], playerPiece, false))
                {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Returns the winning ladder pattern(s) found at position (rowIndex,
     * columnIndex) for a playerPiece GamePiece.
     * 
     * @param rowIndex
     * @param columnIndex
     * @param playerPiece
     * @return result a 2d Array of Slot describing the ladder pattern(s) found
     *         such that :
     * 
     *         result[0] = array of slots describing the left ladder pattern,
     *         sorted from lower-rightmost to upper-leftmost (or null if no
     *         pattern is found)
     * 
     *         result[1] = array of slots describing the right ladder pattern,
     *         sorted from lower-leftmost to upper-rightmost (or null if no
     *         pattern is found)
     */
    private Slot[][] findLadderPatternsAtPosition(int rowIndex, int columnIndex, GamePiece playerPiece)
    {
        Slot[][] result = new Slot[2][];

        Slot slot = Slot.get(rowIndex, columnIndex);
        if (this.getGamePieceAtSlot(slot) == playerPiece)
        {
            try
            {
                // Checking if there is a left ladder pattern
                Slot[] rightAdjacentSlots = slot.getRightAndUpperAdjacentSlots();
                Slot upperleftSlot = slot.getUpperLeftSlot();
                Slot topLadderSlot = upperleftSlot.getUpperSlot();

                if (this.allGamePieceAtSlotsEqualTo(playerPiece, rightAdjacentSlots) && this.getGamePieceAtSlot(upperleftSlot) == playerPiece
                        && this.getGamePieceAtSlot(topLadderSlot) == playerPiece)
                {
                    result[0] = new Slot[5];
                    result[0][0] = rightAdjacentSlots[0];
                    result[0][1] = slot;
                    result[0][2] = rightAdjacentSlots[1];
                    result[0][3] = upperleftSlot;
                    result[0][4] = topLadderSlot;
                }
            }
            catch (NullPointerException e)
            {
                result[0] = null;
            }

            try
            {
                // Checking if there is a right ladder pattern
                Slot[] leftAdjacentSlots = slot.getLeftAndUpperAdjacentSlots();
                Slot upperRightSlot = slot.getUpperRightSlot();
                Slot topLadderSlot = upperRightSlot.getUpperSlot();

                if (this.allGamePieceAtSlotsEqualTo(playerPiece, leftAdjacentSlots) && this.getGamePieceAtSlot(upperRightSlot) == playerPiece
                        && this.getGamePieceAtSlot(topLadderSlot) == playerPiece)
                {
                    result[1] = new Slot[5];
                    result[1][0] = leftAdjacentSlots[0];
                    result[1][1] = slot;
                    result[1][2] = leftAdjacentSlots[1];
                    result[1][3] = upperRightSlot;
                    result[1][4] = topLadderSlot;
                }
            }
            catch (NullPointerException e)
            {
                result[1] = null;
            }
        }

        return result;
    }

    /**
     * returns true if the passed ladderPattern is neutralized by two (or more)
     * of the opponent's GamePiece, false otherwise.
     * 
     * @param ladderPattern
     * @param playerPiece
     *            the GamePiece type of the ladderPattern
     * @param isLeftLadder
     *            true if ladderPattern describes a left ladder pattern, false
     *            if it describes a right ladderPattern
     * @return
     * @throws Exception
     */
    private boolean ladderIsNeutralized(Slot[] ladderPattern, GamePiece playerPiece, boolean isLeftLadder) throws Exception
    {
        GamePiece opponentPiece = LadderGameHelper.getOpponentGamePiece(playerPiece);

        if (isLeftLadder)
        {
            return  this.allGamePieceAtSlotsEqualTo(opponentPiece, ladderPattern[2].getUpperRightAndLowerLeftAdjacentSlots())
                    && ladderPattern[0].getRowIndex() != 0;
        }
        else
        {
            return this.allGamePieceAtSlotsEqualTo(opponentPiece, ladderPattern[2].getUpperLeftAndLowerRightAdjacentSlots())
                    && ladderPattern[0].getRowIndex() != 0;
        }
    }

    /**
     * returns true if all GamePieces found at the passed slots are equal to the
     * passed gamePiece.
     * 
     * @param gamePiece
     * @param slots
     * @return
     */
    private boolean allGamePieceAtSlotsEqualTo(GamePiece gamePiece, Slot[] slots)
    {
        for (Slot slot : slots)
        {
            if (slot == null || this.getGamePieceAtSlot(slot) != gamePiece)
            {
                return false;
            }
        }
        return true;
    }

    /**
     * Changes the current Player to the other Player.
     * 
     * @return the Player who has just finished his turn.
     * @throws Exception
     */
    private Player switchTurn() throws Exception
    {
        if (this.currentPlayer == this.blackPlayer)
        {
            this.currentPlayer = this.whitePlayer;
            return this.blackPlayer;
        }
        else if (this.currentPlayer == this.whitePlayer)
        {
            this.currentPlayer = this.blackPlayer;
            return this.whitePlayer;
        }
        else
        {
            throw new Exception("This Player does not Exists");
        }
    }

    public GamePiece getGamePieceAtSlot(Slot slot)
    {
        return this.grid.get(slot.getRowIndex()).get(slot.getColumnIndex());
    }

    public ArrayList<ArrayList<GamePiece>> getGrid()
    {
        return grid;
    }

    public void setListener(LadderGameListener listener)
    {
        this.listener = listener;
    }

    public Player getCurrentPlayer()
    {
        return currentPlayer;
    }

    public boolean gameIsOver()
    {
        return gameIsOver;
    }
}
